/*
 * AnimationCanvas.java
 *
 * Created on December 3, 2007, 11:45 AM
 *
 */

package ui;

import core.Constants;
import core.IRobot;
import core.MathVector;
import core.Point3D;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferStrategy;

/**
 * The canvas where animation is shown
 *
 * @author  Abhishek Dutta (adutta.np@gmail.com)
 * @version $Id: AnimationCanvas.java 36 2008-01-06 12:06:25Z thelinuxmaniac $
 */

public class AnimationCanvas extends Canvas implements Runnable {
    // location of eye
    private float eyeX;
    private float eyeY;
    private float eyeZ;
    
    private float lookX;
    private float lookY;
    private float lookZ;
    
    // eye point always lies on a sphere
    private float d;
    private float theta;
    private float phi;
    
    // vectors for camera spatial orientation
    private MathVector upVector;
    private MathVector r,u,n;
    
    // k = near/far = 0.1/1000 [scale factor]
    private float k,near,far;
    // width and height of final image
    private float width,height;
    
    // orgin for screen coordinate axes
    private int screenOriginX;
    private int screenOriginY;
    
    public static float dx = 10.0F;
    public static int CUBE_POINTS = 8;
    private float[][] cube;
    
    private int[] cubeProjX;
    private int[] cubeProjY;
    
    // projected control points
    private int[] controlProjX;
    private int[] controlProjY;
    
    private float[] pointsX;
    private float[] pointsY;
    private float[] pointsZ;
    
    // variables that control the formation of sphere along whose latitude and
    // longitude, the camera position moves.
    // see the method calculateCameraPosition()
    public static Float CAMERA_STEP_RES = (float) Math.PI/50;
    public static Integer CAMERA_UP = 10;
    public static Integer CAMERA_DOWN = 11;
    public static Integer CAMERA_LEFT = 12;
    public static Integer CAMERA_RIGHT = 13;
    public static Integer CAMERA_IN = 14;
    public static Integer CAMERA_OUT = 15;
    public static Integer CAMERA_POS_RESET = 16;
    
    // Buffer strategy for the canvas
    private BufferStrategy bufferStrategy;
    
    private boolean runAnimation;
    private boolean redrawCanvas;
    
    public static Integer EYE_POINT = 0;
    public static Integer LOOK_POINT = 1;
    public static Integer WORLD_ORIGIN_POINT = 2;
    public static Integer PERSPECTIVE_POINT = 3;
    public static Integer NO_OF_CONTROL_POINTS = 1;
    
    // hints for rendering of graphics
    private RenderingHints hintForSpeedProcessing;
    private RenderingHints hintForQualityProcessing;
    
    private IRobot robot=null;
    private Thread robotThread;
    private int frame;
    
    /**
     * Creates a new instance of AnimationCanvas
     */
    public AnimationCanvas() {
        
        setBackground(Color.WHITE);
//        setMaximumSize(new java.awt.Dimension(800, 480));
        setMinimumSize(new java.awt.Dimension(800, 480));
        setPreferredSize(new java.awt.Dimension(800, 480));
        
        setHintForSpeedProcessing(new RenderingHints(null));
        getHintForSpeedProcessing().put(RenderingHints.KEY_RENDERING,RenderingHints.VALUE_RENDER_SPEED);
        getHintForSpeedProcessing().put(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_OFF);
        
        setHintForQualityProcessing(new RenderingHints(null));
        getHintForQualityProcessing().put(RenderingHints.KEY_RENDERING,RenderingHints.VALUE_RENDER_QUALITY);
        getHintForQualityProcessing().put(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
        
        runAnimation = true;
        redrawCanvas = true;
        
        screenOriginX = 0;
        screenOriginY = 0;
        
        initParameters();
        
        robot=new IRobot(this);
        frame =-100;
        robotThread = new Thread(this.robot);
        robotThread.setName("RobotThread");
        robotThread.start();
    }
    
    public void refreshCanvas() {
        redrawCanvas = true;
    }

    public void refreshCanvas(int a) {
        redrawCanvas = true;
        this.frame=a;
    }
    
    public void run() {
        while(runAnimation) {
            if(isRedrawCanvas())    {
                this.createBufferStrategy(2);
                bufferStrategy = this.getBufferStrategy();
                Graphics g = null;
                
                g = bufferStrategy.getDrawGraphics();
                Graphics2D g2 = (Graphics2D) g;
                
                /**
                 * NOTE: the the graphics operations become slow, switch to
                 * hints for speed processing
                 * g2.setRenderingHints(getHintForSpeedProcessing());
                 */
                g2.setRenderingHints(getHintForQualityProcessing());
//                g2.setRenderingHints(getHintForSpeedProcessing());
                g2.translate(150,200);

                if(frame!=-100){ 
                    robot.paint(g2,frame); 
                    frame=-100;
                }
                else    {
                    robot.paint(g2);
                }
                
                //drawAxes(g2);
//                *******************
//                initCube();
//                projectCube(g2);
//                showCoordinates("####");
//                *******************
                g.dispose();
                bufferStrategy.show();
                setRedrawCanvas(false);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException ex) {
                    ex.printStackTrace();
                }
            }
        }
    }
    
    
    
    /**
     * Initialize the viewing parameters
     */
    public void initParameters()    {
        upVector = new MathVector(0.0F, 0.0F, 1.0F);
        
        lookX = 0.0F;
        lookY = 0.0F;
        lookZ = Constants.TOTAL_BODY_HEIGHT - Constants.HEAD_HEIGHT - Constants.NECK_HEIGHT - Constants.BODY_HEIGHT/2;
        
        d = 600.0F;
        theta = (float) (Math.PI-Math.PI/9);
        phi = (float) (Math.PI-Math.PI/9);
        calculateEyePosition();
        
        width = 800.0F;
        height = 460.0F;
        
        near = 0.1F;
        far = 1000.0F;
    }
    
    /**
     * Grpahics pipeline for animation. It returns x and y coordinates in
     * xPoints[] and yPoints[] ready to be plotted on the screen :)
     */
    public void graphicsPipeLine(float[] xPoints, float[] yPoints, float[] zPoints, int noOfPoints)  {
        performTranslation(xPoints, yPoints, zPoints, noOfPoints);
        performRotation(xPoints, yPoints, zPoints, noOfPoints);
        performScale(xPoints, yPoints, zPoints, noOfPoints);
        performPerspective(xPoints, yPoints, zPoints, noOfPoints);
        //transformToImageSpace(xPoints, yPoints, zPoints, noOfPoints);
    }
    
    /**
     * performs translation
     */
    public void performTranslation(float[] xPoints, float[] yPoints, float[] zPoints, int noOfPoints)   {
        for (int i = 0; i < noOfPoints; i++) {
            xPoints[i] = xPoints[i] + (-eyeX);
            yPoints[i] = yPoints[i] + (-eyeY);
            zPoints[i] = zPoints[i] + (-eyeZ);
        }
    }
    
    /**
     * performs rotation
     */
    public void performRotation(float[] xPoints, float[] yPoints, float[] zPoints, int noOfPoints)   {
        // r, u, n are orthogonal, unit length vectors
        n = new MathVector(lookX-eyeX, lookY-eyeY, lookZ-eyeZ);
        n = n.getUnitVector();
        
        r = new MathVector(MathVector.crossProduct(upVector, n));
        r = r.getUnitVector();
        
        u = new MathVector(MathVector.crossProduct(n,r));
        u = u.getUnitVector();
        
        float x,y,z;
        for (int i = 0; i < noOfPoints; i++) {
            x = xPoints[i];
            y = yPoints[i];
            z = zPoints[i];
            
            xPoints[i] = x*r.getI() + y*r.getJ() + z*r.getK();
            yPoints[i] = x*u.getI() + y*u.getJ() + z*u.getK();
            zPoints[i] = x*n.getI() + y*n.getJ() + z*n.getK();
        }
    }
    
    /**
     * performs scale operation
     */
    public void performScale(float[] xPoints, float[] yPoints, float[] zPoints, int noOfPoints)  {
        float hByW = height/width;
        for (int i = 0; i < noOfPoints; i++) {
            xPoints[i] = xPoints[i]*(hByW/far);
            yPoints[i] = yPoints[i]*(1/far);
            zPoints[i] = zPoints[i]*(1/far);
        }
    }
    
    /**
     * performs perspective projection
     * NOTE: w = -z
     */
    public void performPerspective(float[] xPoints, float[] yPoints, float[] zPoints, int noOfPoints)    {
        k = near/far;
        for (int i = 0; i < noOfPoints; i++) {
            // no change in xPoints and yPoints
            zPoints[i] = (zPoints[i] + k)/(k-1);
            // NOTE: w = -z
        }
    }
    
    /**
     * converts the projected coordinates to images space of dimension widthxheight
     */
    public void transformToImageSpace(float[] xPoints, float[] yPoints, float[] zPoints, int noOfPoints) {
        float x,y,w;
        for (int i = 0; i < xPoints.length; i++) {
            x = xPoints[i];
            y = yPoints[i];
            w = zPoints[i];
            // NOTE the w=-z in peformPerspective()
            xPoints[i] = width*(((x/w)+1)/2);
            yPoints[i] = height*(((y/w)+1)/2);
        }
    }
    
    /**
     * The UP(DOWN) and RIGHT(LEFT) operations of camera position movement shall
     * move the camera along the longitude and latitude (respectively) of a sphere
     * of radius 'r'. For IN(OUT) operations, the radius 'r' of the sphere will
     * varied.
     * Eye is a point on sphere
     * r = distance, theta = latitude, phi = longitude
     * See Pg. No. 620 of book "Computer Graphics - C version" by: Hearn & Baker
     */
    public void calculateCameraPosition(Integer cameraAction)   {
        // theta = u, phi = v
        
        // reset the value of theta such that theta lies between -2PI and +2PI
        if(theta > (2*Math.PI-CAMERA_STEP_RES) || theta < -(2*Math.PI-CAMERA_STEP_RES)) {
                theta = 0.0F;
        }
        if(cameraAction == CAMERA_UP || cameraAction == CAMERA_DOWN)   {
            if(cameraAction == CAMERA_UP)
                theta = theta - CAMERA_STEP_RES;
            else
                theta = theta + CAMERA_STEP_RES;
            
            // fix the up vector considering the camera movement
            float deg = Math.abs((float) ((theta*180.0F)/Math.PI));
            if(deg <90.0F || deg >270.0F)
                upVector.setK(-1.0F);
            else
                upVector.setK(1.0F);
        }
        else if(cameraAction == CAMERA_LEFT)
            phi = phi - CAMERA_STEP_RES;
        else if(cameraAction == CAMERA_RIGHT)
            phi = phi + CAMERA_STEP_RES;
        else if(cameraAction == CAMERA_IN)
            d = d - CAMERA_STEP_RES*50;
        else if(cameraAction == CAMERA_OUT)
            d = d + CAMERA_STEP_RES*50;
        
        // recalculate the position of eye (camera) such the new position remains
        // on the surface of sphere of radius 'r'
        calculateEyePosition();
    }
    
    /**
     * re-calculates the position of eye based on r,theta, phi parameters of the
     * sphere on which camera is present
     */
    public void calculateEyePosition()  {
        eyeX = d* ((float) (Math.cos(theta)*Math.cos(phi)));
        eyeZ = d* ((float) (Math.sin(theta)));
        eyeY = d* ((float) (Math.sin(phi)*Math.cos(theta)));
    }
    
    /**
     * Project the cube
     */
    public void projectCube(Graphics2D g2)   {
        pointsX = new float[CUBE_POINTS];
        pointsY = new float[CUBE_POINTS];
        pointsZ = new float[CUBE_POINTS];
        
        for (int i = 0; i < CUBE_POINTS; i++) {
            pointsX[i] = cube[i][0];
            pointsY[i] = cube[i][1];
            pointsZ[i] = cube[i][2];
        }
        
        // control points like look point, eye, perspective point
        float[] controlX = new float[NO_OF_CONTROL_POINTS];
        float[] controlY = new float[NO_OF_CONTROL_POINTS];
        float[] controlZ = new float[NO_OF_CONTROL_POINTS];
        // fill the eye point
        controlX[EYE_POINT] = eyeX;
        controlY[EYE_POINT] = eyeY;
        controlZ[EYE_POINT] = eyeZ;
        
        // pass all the 3D world space coordinates through the graphics pipeline
        graphicsPipeLine(pointsX, pointsY, pointsZ, CUBE_POINTS);
        // also convert the control points (eye point, look point, etc)
        graphicsPipeLine(controlX, controlY, controlZ, NO_OF_CONTROL_POINTS);
        
        /*
         * display the projected coordinates
         */
        // convert to integer as g2.drawPolyLine() only takes int[]
        for (int i = 0; i < CUBE_POINTS; i++) {
            cubeProjX[i] = (int) pointsX[i];
            cubeProjY[i] = (int) pointsY[i];
        }
        
        // paint one surface of the cube so that it is easy to visualize the camera
        // movement
        int[] face1X = {cubeProjX[0],cubeProjX[1],cubeProjX[2],cubeProjX[3],cubeProjX[0]};
        int[] face1Y = {cubeProjY[0],cubeProjY[1],cubeProjY[2],cubeProjY[3],cubeProjY[0]};
        
        int[] face2X = {cubeProjX[4],cubeProjX[5],cubeProjX[6],cubeProjX[7],cubeProjX[4]};
        int[] face2Y = {cubeProjY[4],cubeProjY[5],cubeProjY[6],cubeProjY[7],cubeProjY[4]};
        
        int[] face3X = {cubeProjX[0],cubeProjX[1],cubeProjX[5],cubeProjX[4],cubeProjX[0]};
        int[] face3Y = {cubeProjY[0],cubeProjY[1],cubeProjY[5],cubeProjY[4],cubeProjY[0]};
        
        int[] face4X = {cubeProjX[2],cubeProjX[3],cubeProjX[7],cubeProjX[6],cubeProjX[2]};
        int[] face4Y = {cubeProjY[2],cubeProjY[3],cubeProjY[7],cubeProjY[6],cubeProjY[2]};
        
        // fill the cube faces
        g2.setColor(Color.PINK);
        g2.fillPolygon(face1X, face1Y, 5);
        g2.setColor(Color.CYAN);
        g2.fillPolygon(face2X, face2Y, 5);
        g2.setColor(Color.MAGENTA);
        g2.fillPolygon(face3X, face3Y, 5);
        g2.setColor(Color.ORANGE);
        g2.fillPolygon(face4X, face4Y, 5);
        
        // draw the outline
        g2.setColor(Color.BLACK);
        g2.drawPolygon(face1X, face1Y, 5);
        g2.drawPolygon(face2X, face2Y, 5);
        g2.drawPolygon(face3X, face3Y, 5);
        g2.drawPolygon(face4X, face4Y, 5);
        
        // also plot the control points
        g2.setColor(Color.GREEN);
        for (int i = 0; i < NO_OF_CONTROL_POINTS; i++) {
            controlProjX[i] = (int) controlX[i];
            controlProjY[i] = (int) controlY[i];
            g2.fill3DRect(controlProjX[i], controlProjY[i], 4,4,true);
            if(i == EYE_POINT)  {
                g2.drawString("EYE_POINT", controlX[i], controlY[i]);
            } else if(i == LOOK_POINT)  {
                g2.drawString("LOOK_POINT", controlX[i], controlY[i]);
            } else if(i == WORLD_ORIGIN_POINT)  {
                g2.drawString("WORLD_ORIGIN", controlX[i], controlY[i]+10);
//                System.out.println("WORLD-ORG (1)="+getCoordStr("w o. :",controlX[i],controlY[i],controlZ[i]));
            } else if(i == PERSPECTIVE_POINT)  {
                g2.drawString("PERS_POINT", controlX[i], controlY[i]);
            }
        }
        
    }
    
    /**
     * Draw the x, y and z axes on the screen
     */
    public void drawAxes(Graphics2D g2) {
        //axes*[3] = 0.0F describes the origin of world coordinate
        float[] axesX = {60.0F, 0.0F, 0.0F,0.0F};
        float[] axesY = {0.0F, 60.0F, 0.0F,0.0F};
        float[] axesZ = {0.0F, 0.0F, 20.0F,0.0F};
        
        graphicsPipeLine(axesX, axesY, axesZ, 4);
        
        g2.setColor(Color.BLACK);
        // x axis
        g2.drawLine((int) axesX[3],(int) axesY[3],(int) axesX[0],(int) axesX[1]);
        g2.drawString("x axis",(int) axesX[0],(int) axesX[1]);
        
        // y axis
        g2.setColor(Color.BLACK);
        g2.drawLine((int) axesX[3],(int) axesY[3],(int) axesY[0],(int) axesY[1]);
        g2.drawString("y axis",(int) axesY[0],(int) axesY[1]);
        
        // z axis
        g2.setColor(Color.BLACK);
        g2.drawLine((int) axesX[3],(int) axesY[3],(int) axesZ[0],(int) axesZ[1]);
        g2.drawString("z axis",(int) axesZ[0],(int) axesZ[1]);
    }
    
    public String getCoordStr(String cname, float x, float y, float z)    {
        String c = cname+": ("+x+","+y+","+z+")";
        return c;
    }
    
    public String getCoordStr(String cname, float x, float y)    {
        String c = cname+": ("+x+","+y+")";
        return c;
    }
    
    
    public void showCoordinates(String msg)   {
        System.out.print("\n== "+msg+" == "+getCoordStr("EYE",eyeX,eyeY,eyeZ));
        System.out.print("\t"+getCoordStr("SPHERE (d,theta,phi) = ",d,theta,phi)+"\n");
    }
    
    /**
     * Initializes the coordinates of a cube with edge length 'd' with one of the
     * corner at origin
     */
    private void initCube() {
        cube = new float[CUBE_POINTS][];
        cubeProjX = new int[CUBE_POINTS];
        cubeProjY = new int[CUBE_POINTS];
        
        controlProjX = new int[NO_OF_CONTROL_POINTS];
        controlProjY = new int[NO_OF_CONTROL_POINTS];
        
        // initialize the cube points to 0
        for (int i = 0; i < CUBE_POINTS; i++) {
            cube[i] = new float[3];
            cube[i][0] = 0.0F;
            cube[i][1] = 0.0F;
            cube[i][2] = 0.0F;
            
            cubeProjX[i] = 0;
            cubeProjY[i] = 0;
        }
        // initialize the control points to 0
        for (int i = 0; i < NO_OF_CONTROL_POINTS; i++) {
            controlProjX[i] = 0;
            controlProjY[i] = 0;
        }
        // Node 1
        cube[0][0] = dx;
        cube[0][1] = dx;
        cube[0][2] = dx;
        // Node 2
        cube[1][0] = dx;
        cube[1][1] = 0;
        cube[1][2] = dx;
        // Node 3
        cube[2][0] = 0;
        cube[2][1] = 0;
        cube[2][2] = dx;
        // Node 4
        cube[3][0] = 0;
        cube[3][1] = dx;
        cube[3][2] = dx;
        // Node 5
        cube[4][0] = dx;
        cube[4][1] = dx;
        cube[4][2] = 0;
        // Node 6
        cube[5][0] = dx;
        cube[5][1] = 0;
        cube[5][2] = 0;
        // Node 7
        cube[6][0] = 0;
        cube[6][1] = 0;
        cube[6][2] = 0;
        // Node 8
        cube[7][0] = 0;
        cube[7][1] = dx;
        cube[7][2] = 0;
    }
    
    /*
     * output the coordinates of cube to the console
     */
    public void printCubeCoord(String str)   {
        String finalStr = "";
        String p = "";
        for (int i = 0; i < CUBE_POINTS; i++) {
            finalStr += getCoordStr(new Integer(i+1).toString(),pointsX[i],pointsY[i],pointsZ[i])+" ";
        }
        System.out.println("[ "+str+" ] -> "+finalStr);
    }
    
    public boolean isRunAnimation() {
        return runAnimation;
    }
    
    public void setRunAnimation(boolean runAnimation) {
        this.runAnimation = runAnimation;
    }
    
    public boolean isRedrawCanvas() {
        return redrawCanvas;
    }
    
    public void setRedrawCanvas(boolean redrawCanvas) {
        this.redrawCanvas = redrawCanvas;
    }
    
    public RenderingHints getHintForSpeedProcessing() {
        return hintForSpeedProcessing;
    }
    
    public void setHintForSpeedProcessing(RenderingHints hintForSpeedProcessing) {
        this.hintForSpeedProcessing = hintForSpeedProcessing;
    }
    
    public RenderingHints getHintForQualityProcessing() {
        return hintForQualityProcessing;
    }
    
    public void setHintForQualityProcessing(RenderingHints hintForQualityProcessing) {
        this.hintForQualityProcessing = hintForQualityProcessing;
    }
    
    public float getEyeX() {
        return eyeX;
    }
    
    public float getEyeY() {
        return eyeY;
    }
    
    public float getEyeZ() {
        return eyeZ;
    }
    
    public MathVector getN() {
        return n;
    }

    public IRobot getRobot() {
        return robot;
    }

    public void setRobot(IRobot robot) {
        this.robot = robot;
    }
    
    public void setLookPoint(Point3D p) {
        this.lookX = p.getX();
        this.lookY = p.getY();
        this.lookZ = p.getZ();
    }

    public int getFrame() {
        return frame;
    }

    public void setFrame(int frame) {
        this.frame = frame;
    }

    public Thread getRobotThread() {
        return robotThread;
    }

    public void setRobotThread(Thread robotThread) {
        this.robotThread = robotThread;
    }
}